﻿using System;
using System.IO.MemoryMappedFiles;
using System.Threading;

namespace Pfz.Remoting
{
	/// <summary>
	/// This class represents a communication stream based on MemoryMappedFiles. It only works locally, but
	/// is faster than TCP/IP or NamedPipes.
	/// </summary>
	public sealed class MemoryMappedFileWriter:
		IDisposable
	{
		private MemoryMappedFile _file;
		private MemoryMappedViewAccessor _accessor;
		private EventWaitHandle _sentEvent;
		private EventWaitHandle _readEvent;
		private int _mappedFileLength;
		private unsafe byte *_pointer;
		private unsafe MemoryMappedFileReaderWriterData *_data;
		private bool _eventSet;

		/// <summary>
		/// Creates a new MemoryMappedFileWriter with the given name and length, and optionally with the given
		/// timeout, which will be the time it will wait for client connections.
		/// </summary>
		public MemoryMappedFileWriter(string name, int length, int connectionTimeout = -1)
		{
			if (name == null)
				throw new ArgumentNullException("name");

			if (length <= 0)
				throw new ArgumentOutOfRangeException("length");

			if (connectionTimeout < -1)
				throw new ArgumentOutOfRangeException("connectionTimeout");

			unsafe
			{
				_file = MemoryMappedFile.CreateNew(name, length + sizeof(MemoryMappedFileReaderWriterData));
				_accessor = _file.CreateViewAccessor();
				byte *pointer = null;
				_accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
				(*(int *)pointer) = length;
				_mappedFileLength = length;

				_pointer = pointer;
				_data = (MemoryMappedFileReaderWriterData *)(pointer + length);
			}
			_accessor.Flush();

			_sentEvent = new EventWaitHandle(false, EventResetMode.AutoReset, name + ":Sent");
			_sentEvent.Set();

			_readEvent = new EventWaitHandle(false, EventResetMode.AutoReset, name + ":Read");
			if (!_readEvent.WaitOne(connectionTimeout))
				throw new TimeoutException("Timed-out waiting for client connection.");
		}

		/// <summary>
		/// Releases all resources used by this Memory-Mapped-File.
		/// </summary>
		public void Dispose()
		{
			var sentEvent = _sentEvent;

			unsafe
			{
				if (_data != null && sentEvent != null)
				{
					_data->_ended = true;

					sentEvent.Set();
				}
			}

			var accessor = _accessor;
			if (accessor != null)
			{
				_accessor = null;
				accessor.Dispose();
			}

			var file = _file;
			if (file != null)
			{
				_file = null;
				file.Dispose();
			}

			if (sentEvent != null)
			{
				_sentEvent = null;
				sentEvent.Dispose();
			}

			var readEvent = _readEvent;
			if (readEvent != null)
			{
				_readEvent = null;
				readEvent.Dispose();
			}
		}

		private int _timeout = 60000;
		/// <summary>
		/// Gets or sets the time-out used for writes.
		/// This is not the same as the connection time out and must be set explicity.
		/// </summary>
		public int Timeout
		{
			get
			{
				return _timeout;
			}
			set
			{
				if (value < -1)
					throw new ArgumentOutOfRangeException("value");

				_timeout = value;
			}
		}

		/// <summary>
		/// Gets the Length of the memory-mapped-file.
		/// </summary>
		public int MemoryMappedFileLength
		{
			get
			{
				return _mappedFileLength;
			}
		}

		/// <summary>
		/// Writes the buffer data.
		/// </summary>
		public void Write(byte[] buffer)
		{
			if (buffer == null)
				throw new ArgumentNullException("buffer");

			_Write(buffer, 0, buffer.Length);
		}

		/// <summary>
		/// Writes the buffer data.
		/// </summary>
		public void Write(byte[] buffer, int offset)
		{
			if (buffer == null)
				throw new ArgumentNullException("buffer");

			if (offset < 0 || offset >= buffer.Length)
				throw new ArgumentOutOfRangeException("offset");

			_Write(buffer, offset, buffer.Length - offset);
		}

		/// <summary>
		/// Writes the buffer data.
		/// </summary>
		public void Write(byte[] buffer, int offset, int count)
		{
			if (buffer == null)
				throw new ArgumentNullException("buffer");

			if (offset < 0 || offset > buffer.Length)
				throw new ArgumentOutOfRangeException("offset");

			if (count < 0 || offset + count > buffer.Length)
				throw new ArgumentOutOfRangeException("count");

			_Write(buffer, offset, count);
		}
		private void _Write(byte[] buffer, int offset, int count)
		{
			unsafe
			{
				while(count > 0)
				{
					int writerPosition = _data->_writerPosition;
					bool writerState = _data->_writerState;
					int readerPosition;
					bool readerState;

					while (true)
					{
						readerPosition = _data->_readerPosition;
						readerState = _data->_readerState;

						if (writerPosition != readerPosition || writerState == readerState)
							break;

						_eventSet = false;
						_sentEvent.Set();
						if (!_readEvent.WaitOne(_timeout))
							throw new TimeoutException("Write timed-out.");
					}

					int remaining;
					if (readerState == writerState)
						remaining = _mappedFileLength - writerPosition;
					else
						remaining = readerPosition - writerPosition;

					int toSend = Math.Min(count, remaining);
					fixed(byte *sourcePointer = buffer)
						UnsafeBuffer.BlockCopy(sourcePointer + offset, _pointer + writerPosition, toSend);

					offset += toSend;
					count -= toSend;

					writerPosition += toSend;
					if (writerPosition >= _mappedFileLength)
					{
						writerPosition = 0;
						_data->_writerState = !writerState;
					}

					_data->_writerPosition = writerPosition;

					if (!_eventSet)
					{
						_eventSet = true;
						_sentEvent.Set();
					}
				}
			}
		}

		/// <summary>
		/// Tells the other side that it can start reading.
		/// </summary>
		public void Flush()
		{
			_sentEvent.Set();
		}

		/// <summary>
		/// Writes a single byte of data.
		/// </summary>
		public void WriteByte(byte value)
		{
			unsafe
			{
				int writerPosition = _data->_writerPosition;
				bool writerState = _data->_writerState;

				while (writerPosition == _data->_readerPosition && writerState != _data->_readerState)
				{
					_eventSet = false;
					_sentEvent.Set();
					if (!_readEvent.WaitOne(_timeout))
						throw new TimeoutException("Write timed-out.");
				}

				_pointer[writerPosition] = value;

				writerPosition ++;
				if (writerPosition >= _mappedFileLength)
				{
					writerPosition = 0;
					_data->_writerState = !writerState;
				}

				_data->_writerPosition = writerPosition;

				if (!_eventSet)
				{
					_eventSet = true;
					_sentEvent.Set();
				}
			}
		}
	}
}
